/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.data.service.segments.records.merge;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import org.springframework.stereotype.Component;
import org.unidata.mdm.core.type.data.RecordStatus;
import org.unidata.mdm.core.type.timeline.TimeInterval;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.data.context.MergeRequestContext;
import org.unidata.mdm.data.convert.RecordIndexingConverter;
import org.unidata.mdm.data.module.DataModule;
import org.unidata.mdm.data.type.apply.RecordMergeChangeSet;
import org.unidata.mdm.data.type.data.EtalonRecord;
import org.unidata.mdm.data.type.data.OriginRecord;
import org.unidata.mdm.data.type.keys.RecordKeys;
import org.unidata.mdm.meta.type.search.EtalonIndexId;
import org.unidata.mdm.meta.type.search.RecordHeaderField;
import org.unidata.mdm.meta.type.search.RecordIndexId;
import org.unidata.mdm.search.configuration.SearchConfigurationConstants;
import org.unidata.mdm.search.context.IndexRequestContext;
import org.unidata.mdm.search.type.id.AbstractManagedIndexId;
import org.unidata.mdm.search.type.id.ManagedIndexId;
import org.unidata.mdm.search.type.indexing.Indexing;
import org.unidata.mdm.search.type.indexing.IndexingField;
import org.unidata.mdm.search.util.SearchUtils;
import org.unidata.mdm.system.type.annotation.ConfigurationRef;
import org.unidata.mdm.system.type.configuration.ConfigurationValue;
import org.unidata.mdm.system.type.pipeline.Point;
import org.unidata.mdm.system.type.pipeline.Start;
import org.unidata.mdm.system.type.runtime.MeasurementPoint;

/**
 * The merge indexing info generator.
 * @author Mikhail Mikhailov on Nov 10, 2019
 */
@Component(RecordMergeIndexingExecutor.SEGMENT_ID)
public class RecordMergeIndexingExecutor extends Point<MergeRequestContext> {
    /**
     * This segment ID.
     */
    public static final String SEGMENT_ID = DataModule.MODULE_ID + "[RECORD_MERGE_INDEXING]";
    /**
     * Localized message code.
     */
    public static final String SEGMENT_DESCRIPTION = DataModule.MODULE_ID + ".record.merge.indexing.description";
    /**
     * Delay for async audit operations.
     */
    @ConfigurationRef(SearchConfigurationConstants.PROPERTY_REFRESH_IMMEDIATE)
    private ConfigurationValue<Boolean> refreshImmediate;
    /**
     * Constructor.
     * @param id
     * @param description
     */
    public RecordMergeIndexingExecutor() {
        super(SEGMENT_ID, SEGMENT_DESCRIPTION);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void point(MergeRequestContext ctx) {

        MeasurementPoint.start();
        try {

            RecordKeys keys = ctx.keys();
            RecordMergeChangeSet cs = ctx.changeSet();

            IndexRequestContext irc = IndexRequestContext.builder()
                    .drop(true)
                    .entity(keys.getEntityName())
                    .delete(collectDeletes(ctx))
                    .index(collectUpdates(ctx))
                    .routing(keys.getEtalonKey().getId())
                    .refresh(!ctx.isBatchOperation() && refreshImmediate.getValue())
                    .build();

            cs.getIndexRequestContexts().add(irc);

        } finally {
            MeasurementPoint.stop();
        }
    }

    private Collection<ManagedIndexId> collectDeletes(MergeRequestContext ctx) {

        List<ManagedIndexId> result = new ArrayList<>();

        RecordKeys keys = ctx.keys();
        Timeline<OriginRecord> current = ctx.currentTimeline();
        current.stream()
                .map(interval -> RecordIndexId.of(keys.getEntityName(), keys.getEtalonKey().getId(), interval.getPeriodId()))
                .collect(Collectors.toCollection(() -> result));

        for (Timeline<OriginRecord> dt : ctx.duplicateTimelines().values()) {

            RecordKeys dk = dt.getKeys();
            dt.stream()
                .map(interval -> RecordIndexId.of(dk.getEntityName(), dk.getEtalonKey().getId(), interval.getPeriodId()))
                .collect(Collectors.toCollection(() -> result));

            result.add(EtalonIndexId.of(dk.getEntityName(), dk.getEtalonKey().getId()));
        }

        return result;
    }

    private Collection<Indexing> collectUpdates(MergeRequestContext ctx) {

        Timeline<OriginRecord> next = ctx.nextTimeline();
        if (next.isEmpty()) {
            return Collections.emptyList();
        }

        RecordKeys keys = ctx.keys();

        boolean isPublished = keys.isPublished();

        Map<EtalonRecord, Collection<IndexingField>> records = new IdentityHashMap<>(next.size());
        for (TimeInterval<OriginRecord> i : next) {

            EtalonRecord etalon = i.getCalculationResult();
            if (Objects.isNull(etalon)) {
                continue;
            }

            Collection<IndexingField> fields = new ArrayList<>(RecordHeaderField.values().length);
            fields.add(IndexingField.of(RecordHeaderField.FIELD_FROM.getName(), i.getValidFrom()));
            fields.add(IndexingField.of(RecordHeaderField.FIELD_TO.getName(), i.getValidTo()));
            fields.add(IndexingField.of(RecordHeaderField.FIELD_CREATED_AT.getName(), etalon.getInfoSection().getCreateDate()));
            fields.add(IndexingField.of(RecordHeaderField.FIELD_UPDATED_AT.getName(), etalon.getInfoSection().getUpdateDate()));
            fields.add(IndexingField.of(RecordHeaderField.FIELD_CREATED_BY.getName(), etalon.getInfoSection().getCreatedBy()));
            fields.add(IndexingField.of(RecordHeaderField.FIELD_UPDATED_BY.getName(), etalon.getInfoSection().getUpdatedBy()));
            fields.add(IndexingField.of(RecordHeaderField.FIELD_DELETED.getName(), keys.getEtalonKey().getStatus() == RecordStatus.INACTIVE));
            fields.add(IndexingField.of(RecordHeaderField.FIELD_INACTIVE.getName(), !i.isActive()));
            fields.add(IndexingField.of(RecordHeaderField.FIELD_ORIGINATOR.getName(), etalon.getInfoSection().getUpdatedBy()));
            fields.add(IndexingField.of(RecordHeaderField.FIELD_ETALON_ID.getName(), keys.getEtalonKey().getId()));
            fields.add(IndexingField.of(RecordHeaderField.FIELD_PERIOD_ID.getName(), AbstractManagedIndexId.periodIdValToString(etalon.getInfoSection().getValidTo())));
            fields.add(IndexingField.of(RecordHeaderField.FIELD_OPERATION_TYPE.getName(), etalon.getInfoSection().getOperationType().name()));
            fields.add(IndexingField.ofStrings(RecordHeaderField.FIELD_EXTERNAL_KEYS.getName(), i.unlock().toCalculables().stream()
                .map(origin -> origin.getSourceSystem() + SearchUtils.COLON_SEPARATOR + origin.getExternalId())
                .collect(Collectors.toList())));

            records.put(etalon, fields);
        }

        return RecordIndexingConverter.convert(records);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean supports(Start<?, ?> start) {
        return MergeRequestContext.class.isAssignableFrom(start.getInputTypeClass());
    }
}
